package edu.northwestern.cbits.purple_robot_manager.probes.services; import java.io.InputStream; import java.net.URL; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.location.places.Place; import com.google.android.gms.location.places.PlaceLikelihood; import com.google.android.gms.location.places.PlaceLikelihoodBuffer; import com.google.android.gms.location.places.Places; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; public class GooglePlacesProbe extends Probe implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private static final String DEFAULT_RADIUS = "1000"; private static final String RADIUS = "config_probe_google_places_radius"; private static final String ENABLED = "config_probe_google_places_enabled"; private static final boolean DEFAULT_ENABLED = false; private static final String MOST_LIKELY_PLACE = "MOST_LIKELY_PLACE"; private static final String MOST_LIKELY_PLACE_ID = "MOST_LIKELY_PLACE_ID"; private static final String MOST_LIKELY_PLACE_LIKELIHOOD = "MOST_LIKELY_PLACE_LIKELIHOOD"; private static String[] EXCLUDED_TYPES = { "establishment" }; protected long _lastCheck = 0; private GoogleApiClient _client = null; @Override public String getPreferenceKey() { return "services_google_places"; } @Override public String summary(Context context) { return context.getString(R.string.summary_google_places_probe_desc); } @Override public String name(Context context) { return "edu.northwestern.cbits.purple_robot_manager.probes.services.GooglePlacesProbe"; } @Override public String title(Context context) { return context.getString(R.string.title_google_places_probe); } @Override public String probeCategory(Context context) { return context.getResources().getString(R.string.probe_external_services_category); } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(GooglePlacesProbe.ENABLED, true); e.commit(); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(GooglePlacesProbe.ENABLED, false); e.commit(); } @Override public boolean isEnabled(final Context context) { if (super.isEnabled(context)) { final SharedPreferences prefs = Probe.getPreferences(context); if (prefs.getBoolean(GooglePlacesProbe.ENABLED, GooglePlacesProbe.DEFAULT_ENABLED)) { long now = System.currentTimeMillis(); if (now - this._lastCheck > 300000) // 5 minutes { this._lastCheck = now; final GooglePlacesProbe me = this; if (this._client == null) { GoogleApiClient.Builder builder = new GoogleApiClient.Builder(context); builder = builder.addApi(Places.GEO_DATA_API); builder = builder.addApi(Places.PLACE_DETECTION_API); builder.addConnectionCallbacks(this); builder.addOnConnectionFailedListener(this); this._client = builder.build(); this._client.connect(); this._lastCheck = 0; } else if (this._client.isConnected()) { PendingResult<PlaceLikelihoodBuffer> result = Places.PlaceDetectionApi.getCurrentPlace(this._client, null); result.setResultCallback(new ResultCallback<PlaceLikelihoodBuffer>() { public void onResult(PlaceLikelihoodBuffer likelyPlaces) { LocationManager locations = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); Location last = null; if (locations.isProviderEnabled(LocationManager.GPS_PROVIDER)) last = locations.getLastKnownLocation(LocationManager.GPS_PROVIDER); else if (locations.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) last = locations.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); else last = locations.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); double radius = Double.parseDouble(prefs.getString(GooglePlacesProbe.RADIUS, GooglePlacesProbe.DEFAULT_RADIUS)); if (last == null) radius = Double.MAX_VALUE; float mostLikelihood = 0; String mostLikelyPlaceName = null; String mostLikelyPlaceId = null; HashMap<String, Integer> places = new HashMap<>(); for (PlaceLikelihood placeLikelihood : likelyPlaces) { Place place = placeLikelihood.getPlace(); Location placeLocation = new Location(LocationManager.PASSIVE_PROVIDER); placeLocation.setLatitude(place.getLatLng().latitude); placeLocation.setLongitude(place.getLatLng().longitude); if (last == null || Math.abs(last.distanceTo(placeLocation)) < radius) { List<Integer> types = place.getPlaceTypes(); for (Integer type : types) { String typeString = GooglePlacesProbe.stringForType(type); Integer count = places.get(typeString); if (count == null) count = 0; count += 1; places.put(typeString, count); } if (placeLikelihood.getLikelihood() > mostLikelihood) { mostLikelyPlaceId = place.getId(); mostLikelyPlaceName = place.getName().toString(); mostLikelihood = placeLikelihood.getLikelihood(); } } } likelyPlaces.release(); Bundle bundle = new Bundle(); bundle.putString("PROBE", me.name(context)); bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000); if (mostLikelyPlaceName != null) { bundle.putString(GooglePlacesProbe.MOST_LIKELY_PLACE, mostLikelyPlaceName); bundle.putString(GooglePlacesProbe.MOST_LIKELY_PLACE_ID, mostLikelyPlaceId); bundle.putFloat(GooglePlacesProbe.MOST_LIKELY_PLACE_LIKELIHOOD, mostLikelihood); } for (String key : places.keySet()) { if (Arrays.asList(GooglePlacesProbe.EXCLUDED_TYPES).contains(key) == false) bundle.putInt(key, places.get(key)); } me.transmitData(context, bundle); } }); } } return true; } } return false; } protected static Map<String, Integer> nearestLocation(Context context, double latitude, double longitude) throws Exception { SharedPreferences prefs = Probe.getPreferences(context); String key = context.getString(R.string.google_places_browser_key); String radius = prefs.getString(GooglePlacesProbe.RADIUS, GooglePlacesProbe.DEFAULT_RADIUS); URL u = new URL("https://maps.googleapis.com/maps/api/place/search/json?location=" + latitude + "," + longitude + "&radius=" + radius + "&sensor=false&key=" + key); InputStream in = u.openStream(); String jsonString = IOUtils.toString(in); in.close(); JSONObject json = new JSONObject(jsonString); JSONArray results = json.getJSONArray("results"); HashMap<String, Integer> place = new HashMap<>(); String[] availableTypes = context.getResources().getStringArray(R.array.google_places_types); for (String type : availableTypes) { place.put(type, 0); } for (int i = 0; i < results.length(); i++) { JSONObject result = results.getJSONObject(i); JSONArray types = result.getJSONArray("types"); for (int j = 0; j < types.length(); j++) { String type = types.getString(j); Integer count = place.get(type); if (count == null) count = 0; count = count.intValue() + 1; place.put(type, count); } } return place; } @Override public String summarizeValue(Context context, Bundle bundle) { String frequentPlace = "none"; int maxCount = 0; for (String key : bundle.keySet()) { Object o = bundle.get(key); if ("TIMESTAMP".equals(key)) { } else if (o instanceof Integer) { Integer count = (Integer) o; if (count > maxCount) { frequentPlace = key; maxCount = count; } } else if (o instanceof Double) { Double count = (Double) o; if (count.intValue() > maxCount) { frequentPlace = key; maxCount = count.intValue(); } } } return String.format(context.getResources().getString(R.string.summary_google_places), frequentPlace.replaceAll("_", " "), maxCount); } @Override public PreferenceScreen preferenceScreen(Context context, PreferenceManager manager) { final PreferenceScreen screen = super.preferenceScreen(context, manager); screen.setTitle(this.title(context)); screen.setSummary(this.summary(context)); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_probe); enabled.setKey(GooglePlacesProbe.ENABLED); enabled.setDefaultValue(GooglePlacesProbe.DEFAULT_ENABLED); screen.addPreference(enabled); FlexibleListPreference radius = new FlexibleListPreference(context); radius.setKey(GooglePlacesProbe.RADIUS); radius.setEntryValues(R.array.feature_google_places_values); radius.setEntries(R.array.feature_google_places_labels); radius.setTitle(R.string.feature_google_places_radius_label); radius.setDefaultValue(GooglePlacesProbe.DEFAULT_RADIUS); screen.addPreference(radius); return screen; } @Override public Map<String, Object> configuration(Context context) { Map<String, Object> map = super.configuration(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); map.put(GooglePlacesProbe.RADIUS, Double.parseDouble(prefs.getString(GooglePlacesProbe.RADIUS, GooglePlacesProbe.DEFAULT_RADIUS))); return map; } @Override public JSONObject fetchSettings(Context context) { JSONObject settings = super.fetchSettings(context); try { JSONObject radius = new JSONObject(); radius.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG); JSONArray values = new JSONArray(); String[] options = context.getResources().getStringArray(R.array.feature_google_places_values); for (String option : options) { values.put(Double.parseDouble(option)); } radius.put(Probe.PROBE_VALUES, values); settings.put(GooglePlacesProbe.RADIUS, radius); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } return settings; } @Override public void updateFromMap(Context context, Map<String, Object> params) { super.updateFromMap(context, params); if (params.containsKey(GooglePlacesProbe.RADIUS)) { Object radius = params.get(GooglePlacesProbe.RADIUS); if (radius instanceof Double) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putString(GooglePlacesProbe.RADIUS, radius.toString()); e.commit(); } } } @Override public void onConnected(Bundle bundle) { } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } private static String stringForType(int type) { switch(type) { case Place.TYPE_ACCOUNTING: return "accounting"; case Place.TYPE_ADMINISTRATIVE_AREA_LEVEL_1: return "administrative_level_1"; case Place.TYPE_ADMINISTRATIVE_AREA_LEVEL_2: return "administrative_level_2"; case Place.TYPE_ADMINISTRATIVE_AREA_LEVEL_3: return "administrative_level_3"; case Place.TYPE_AMUSEMENT_PARK: return "amusement_park"; case Place.TYPE_AQUARIUM: return "aquarium"; case Place.TYPE_ART_GALLERY: return "art_gallery"; case Place.TYPE_ATM: return "atm"; case Place.TYPE_BAKERY: return "bakery"; case Place.TYPE_BANK: return "bank"; case Place.TYPE_BAR: return "bar"; case Place.TYPE_BEAUTY_SALON: return "beauty_salon"; case Place.TYPE_BICYCLE_STORE: return "bicycle_store"; case Place.TYPE_BOWLING_ALLEY: return "bowling_alley"; case Place.TYPE_BUS_STATION: return "bus_station"; case Place.TYPE_CAFE: return "cafe"; case Place.TYPE_CAMPGROUND: return "campground"; case Place.TYPE_CAR_DEALER: return "car_dealer"; case Place.TYPE_CAR_RENTAL: return "car_rental"; case Place.TYPE_CAR_REPAIR: return "car_repair"; case Place.TYPE_CAR_WASH: return "car_wash"; case Place.TYPE_CASINO: return "casino"; case Place.TYPE_CEMETERY: return "cemetery"; case Place.TYPE_CHURCH: return "church"; case Place.TYPE_CITY_HALL: return "city_hall"; case Place.TYPE_CLOTHING_STORE: return "clothing_store"; case Place.TYPE_COLLOQUIAL_AREA: return "colloquial_area"; case Place.TYPE_CONVENIENCE_STORE: return "convenience_store"; case Place.TYPE_COURTHOUSE: return "courthouse"; case Place.TYPE_DENTIST: return "dentist"; case Place.TYPE_DEPARTMENT_STORE: return "department_store"; case Place.TYPE_DOCTOR: return "doctor"; case Place.TYPE_ELECTRICIAN: return "electrician"; case Place.TYPE_ELECTRONICS_STORE: return "electronics_store"; case Place.TYPE_EMBASSY: return "embassy"; case Place.TYPE_ESTABLISHMENT: return "establishment"; case Place.TYPE_FINANCE: return "finance"; case Place.TYPE_FIRE_STATION: return "fire_station"; case Place.TYPE_FLOOR: return "floor"; case Place.TYPE_FLORIST: return "florist"; case Place.TYPE_FOOD: return "food"; case Place.TYPE_FUNERAL_HOME: return "funeral_home"; case Place.TYPE_FURNITURE_STORE: return "furniture_store"; case Place.TYPE_GAS_STATION: return "gas_station"; case Place.TYPE_GENERAL_CONTRACTOR: return "general_contractor"; case Place.TYPE_GEOCODE: return "geocode"; case Place.TYPE_GROCERY_OR_SUPERMARKET: return "grocery_or_supermarket"; case Place.TYPE_GYM: return "gym"; case Place.TYPE_HAIR_CARE: return "hair_care"; case Place.TYPE_HARDWARE_STORE: return "hardware_store"; case Place.TYPE_HEALTH: return "health"; case Place.TYPE_HINDU_TEMPLE: return "hindu_temple"; case Place.TYPE_HOME_GOODS_STORE: return "home_goods_store"; case Place.TYPE_HOSPITAL: return "hospital"; case Place.TYPE_INSURANCE_AGENCY: return "insurance_agency"; case Place.TYPE_INTERSECTION: return "intersection"; case Place.TYPE_JEWELRY_STORE: return "jewelry_store"; case Place.TYPE_LAUNDRY: return "laundry"; case Place.TYPE_LAWYER: return "lawyer"; case Place.TYPE_LIBRARY: return "library"; case Place.TYPE_LIQUOR_STORE: return "liquor_store"; case Place.TYPE_LOCALITY: return "locality"; case Place.TYPE_LOCAL_GOVERNMENT_OFFICE: return "local_government_office"; case Place.TYPE_LOCKSMITH: return "locksmith"; case Place.TYPE_LODGING: return "lodging"; case Place.TYPE_MEAL_DELIVERY: return "meal_delivery"; case Place.TYPE_MEAL_TAKEAWAY: return "meal_takeaway"; case Place.TYPE_MOSQUE: return "mosque"; case Place.TYPE_MOVIE_RENTAL: return "movie_rental"; case Place.TYPE_MOVIE_THEATER: return "movie_theater"; case Place.TYPE_MOVING_COMPANY: return "moving_company"; case Place.TYPE_MUSEUM: return "museum"; case Place.TYPE_NATURAL_FEATURE: return "natural_feature"; case Place.TYPE_NEIGHBORHOOD: return "neighborhood"; case Place.TYPE_NIGHT_CLUB: return "night_club"; case Place.TYPE_OTHER: return "other"; case Place.TYPE_PAINTER: return "painter"; case Place.TYPE_PARK: return "park"; case Place.TYPE_PARKING: return "parking"; case Place.TYPE_PET_STORE: return "pet_store"; case Place.TYPE_PHARMACY: return "pharmacy"; case Place.TYPE_PHYSIOTHERAPIST: return "physiotherapist"; case Place.TYPE_PLACE_OF_WORSHIP: return "place_of_worship"; case Place.TYPE_PLUMBER: return "plumber"; case Place.TYPE_POINT_OF_INTEREST: return "point_of_interest"; case Place.TYPE_POLICE: return "police"; case Place.TYPE_POLITICAL: return "political"; case Place.TYPE_POSTAL_CODE: return "postal_code"; case Place.TYPE_POSTAL_CODE_PREFIX: return "postal_code_prefix"; case Place.TYPE_POSTAL_TOWN: return "postal_town"; case Place.TYPE_POST_BOX: return "post_box"; case Place.TYPE_POST_OFFICE: return "post_office"; case Place.TYPE_PREMISE: return "premise"; case Place.TYPE_REAL_ESTATE_AGENCY: return "real_estate_agency"; case Place.TYPE_RESTAURANT: return "restaurant"; case Place.TYPE_ROOFING_CONTRACTOR: return "roofing_contractor"; case Place.TYPE_ROOM: return "room"; case Place.TYPE_ROUTE: return "route"; case Place.TYPE_RV_PARK: return "rv_park"; case Place.TYPE_SCHOOL: return "school"; case Place.TYPE_SHOE_STORE: return "show_store"; case Place.TYPE_SHOPPING_MALL: return "shopping_mall"; case Place.TYPE_SPA: return "spa"; case Place.TYPE_STADIUM: return "stadium"; case Place.TYPE_STORAGE: return "storage"; case Place.TYPE_STORE: return "store"; case Place.TYPE_STREET_ADDRESS: return "street_address"; case Place.TYPE_SUBLOCALITY: return "sublocality"; case Place.TYPE_SUBLOCALITY_LEVEL_1: return "sublocality_level_1"; case Place.TYPE_SUBLOCALITY_LEVEL_2: return "sublocality_level_2"; case Place.TYPE_SUBLOCALITY_LEVEL_3: return "sublocality_level_3"; case Place.TYPE_SUBLOCALITY_LEVEL_4: return "sublocality_level_4"; case Place.TYPE_SUBLOCALITY_LEVEL_5: return "sublocality_level_5"; case Place.TYPE_SUBPREMISE: return "subpremise"; case Place.TYPE_SUBWAY_STATION: return "subway_station"; case Place.TYPE_SYNAGOGUE: return "synagogue"; case Place.TYPE_SYNTHETIC_GEOCODE: return "synthetic_geocode"; case Place.TYPE_TAXI_STAND: return "taxi_stand"; case Place.TYPE_TRAIN_STATION: return "train_station"; case Place.TYPE_TRANSIT_STATION: return "transit_station"; case Place.TYPE_TRAVEL_AGENCY: return "travel_agency"; case Place.TYPE_UNIVERSITY: return "university"; case Place.TYPE_VETERINARY_CARE: return "veterinary_care"; case Place.TYPE_ZOO: return "zoo"; } return "unknown"; } }